package edu.ssdut.revise.core.matchRule;

import com.github.javaparser.ast.body.*;
import com.github.javaparser.utils.Pair;
import edu.ssdut.revise.core.match.MatchStore;
import edu.ssdut.revise.utils.javaParserUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Iterator;
import java.util.Objects;

/**
 * Defines rules during top-down matching for different types
 */
public class Top2DownMatchRule implements MatchRule {
    private final static Logger logger = LoggerFactory.getLogger(Top2DownMatchRule.class);

    @Override
    public void typeDeclarationMatch(MatchStore<TypeDeclaration<?>> matchStore) {
        for (Iterator<TypeDeclaration<?>> it = matchStore.getUnmatchedNodes1().iterator(); it.hasNext(); ) {
            TypeDeclaration<?> td1 = it.next();
            for (Iterator<TypeDeclaration<?>> it2 = matchStore.getUnmatchedNodes2().iterator(); it2.hasNext(); ) {
                TypeDeclaration<?> td2 = it2.next();
                if (this.ifTypeDeclarationMatch(td1, td2)) {
                    matchStore.getMatched().add(new Pair<>(td1, td2));
                    it.remove();
                    it2.remove();
                }
            }
        }
    }

    /**
     * test if two TypeDeclarations match
     * <br/>with identical name and parents
     */
    public Boolean ifTypeDeclarationMatch(TypeDeclaration<?> a, TypeDeclaration<?> b) {
        if (a instanceof ClassOrInterfaceDeclaration && b instanceof ClassOrInterfaceDeclaration) {
            ClassOrInterfaceDeclaration da = (ClassOrInterfaceDeclaration) a, db = (ClassOrInterfaceDeclaration) b;
            if (da.isInterface() == db.isInterface()) {
                //both classes or both interfaces
                if (da.getNameAsString().equals(db.getNameAsString())) {
                    // TODO:consider adding extendType and so on
                    return ifTopLevelTypeWithIdenticalName(da, db);
                } else {
                    return false;
                }
            }
        } else if (a instanceof EnumDeclaration && b instanceof EnumDeclaration) {  // Enum
            return ifTopLevelTypeWithIdenticalName(a, b);
        }
        logger.warn("TypeDeclarations mismatched from ifTypeDeclarationMatch");
        return false;
    }

    /**
     * if the two TypeDeclarations are top level types with identical names
     */
    private Boolean ifTopLevelTypeWithIdenticalName(TypeDeclaration<?> a, TypeDeclaration<?> b) {
        if (a.isTopLevelType() && b.isTopLevelType() && a.getNameAsString().equals(b.getNameAsString())) {
            return true;
        }
        if (a.getParentNode().isPresent() && b.getParentNode().isPresent()) {
            return javaParserUtils.getNodeName(a.getParentNode().get()).equals(javaParserUtils.getNodeName(b.getParentNode().get()));
        } else {
            return false;
        }
    }


    @Override
    public void variableDeclaratorMatch(MatchStore<VariableDeclarator> matchStore) {
        outer:
        for (Iterator<VariableDeclarator> it = matchStore.getUnmatchedNodes1().iterator(); it.hasNext(); ) {
            VariableDeclarator vd1 = it.next();
            for (Iterator<VariableDeclarator> it2 = matchStore.getUnmatchedNodes2().iterator(); it2.hasNext(); ) {
                VariableDeclarator vd2 = it2.next();
                if (this.ifVariableDeclaratorMatch(vd1, vd2)) {
                    matchStore.getMatched().add(new Pair<>(vd1, vd2));
                    it.remove();
                    it2.remove();
                    continue outer;
                }
            }
        }
    }

    /**
     * test if two VariableDeclarators match
     * <br/>with identical type, identical name and matched TypeDeclaration as parent
     */
    public Boolean ifVariableDeclaratorMatch(VariableDeclarator a, VariableDeclarator b) {
        // identical type requirement leads to failure of matches like float to double
        if (!a.getType().equals(b.getType())) {
            return false;
        }
        // parent matching
        if (a.getParentNode().isPresent() && a.getParentNode().get().getParentNode().isPresent() && b.getParentNode().isPresent() && b.getParentNode().get().getParentNode().isPresent()) {
            if (!(a.getParentNode().get().getParentNode().get() instanceof TypeDeclaration && b.getParentNode().get().getParentNode().get() instanceof TypeDeclaration)) {
                logger.warn("field with parent no type of TypeDeclaration");
                return false;
            }
            TypeDeclaration<?> ap = (TypeDeclaration<?>) a.getParentNode().get().getParentNode().get();
            TypeDeclaration<?> bp = (TypeDeclaration<?>) b.getParentNode().get().getParentNode().get();
            if (!ifTypeDeclarationMatch(ap, bp)) {
                // parent unmatched
                return false;
            }
        }

        // name matching
        // NOTICE that a field can contain many variables
        return Objects.equals(a.getNameAsString(), b.getNameAsString());
    }

    @Override
    public void methodDeclarationMatch(MatchStore<MethodDeclaration> matchStore) {
        outer:
        for (Iterator<MethodDeclaration> it = matchStore.getUnmatchedNodes1().iterator(); it.hasNext(); ) {
            MethodDeclaration md1 = it.next();
            for (Iterator<MethodDeclaration> it2 = matchStore.getUnmatchedNodes2().iterator(); it2.hasNext(); ) {
                MethodDeclaration md2 = it2.next();
                if (this.ifMethodDeclarationMatch(md1, md2)) {
                    matchStore.getMatched().add(new Pair<>(md1, md2));
                    it.remove();
                    it2.remove();
                    continue outer;
                }
            }
        }
    }

    /**
     * test if two MethodDeclarations match
     * <br/>with identical type, identical name, identical parameters and matched TypeDeclaration as parent
     */
    public Boolean ifMethodDeclarationMatch(MethodDeclaration a, MethodDeclaration b) {
        // TODO: what about modifiers
        // identical type
        if (!a.getType().equals(b.getType())) {
            return false;
        }
        // identical name
        if (!a.getNameAsString().equals(b.getNameAsString())) {
            return false;
        }

        // parent TypeDeclaration match
        if (a.getParentNode().isEmpty() || b.getParentNode().isEmpty()) {
            logger.warn("method with no parent");
            return false;
        }
        if (!(a.getParentNode().get() instanceof TypeDeclaration && b.getParentNode().get() instanceof TypeDeclaration)) {
            logger.warn("method with parent no type of TypeDeclaration");
            return false;
        }
        TypeDeclaration<?> ap = (TypeDeclaration<?>) a.getParentNode().get();
        TypeDeclaration<?> bp = (TypeDeclaration<?>) b.getParentNode().get();
        if (!ifTypeDeclarationMatch(ap, bp)) {
            // parent unmatched
            return false;
        }
        // paremeters match (only match identical parameter list, but order change is ok)
        return a.getParameters().size() == b.getParameters().size()
                &&
                a.getParameters().containsAll(b.getParameters());
    }

    @Override
    public void constructorDeclarationMatch(MatchStore<ConstructorDeclaration> matchStore) {
        outer:
        for (Iterator<ConstructorDeclaration> it = matchStore.getUnmatchedNodes1().iterator(); it.hasNext(); ) {
            ConstructorDeclaration cd1 = it.next();
            for (Iterator<ConstructorDeclaration> it2 = matchStore.getUnmatchedNodes2().iterator(); it2.hasNext(); ) {
                ConstructorDeclaration cd2 = it2.next();
                if (this.ifConstructorDeclarationMatch(cd1, cd2)) {
                    matchStore.getMatched().add(new Pair<>(cd1, cd2));
                    it.remove();
                    it2.remove();
                    continue outer;
                }
            }
        }
    }

    /**
     * test if two ConstructorDeclarations match
     * <br/>with identical parameter list
     */
    public Boolean ifConstructorDeclarationMatch(ConstructorDeclaration a, ConstructorDeclaration b) {
        // identical name
        if (!a.getNameAsString().equals(b.getNameAsString())) {
            return false;
        }
        // parent matching process deleted

        // paremeters match (only match identical parameter list, but order change is ok)
        // TODO: cannot cope with the situation where field is renamed
        return a.getParameters().size() == b.getParameters().size()
                &&
                a.getParameters().containsAll(b.getParameters());
    }


}